Completed
Branch v10.2.x (c81ed9)
by Rafael S.
16:21
created

WaveFileConverter.correctContainer_   A

Complexity

Conditions 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 2
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileConverter class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { changeBitDepth as bitDepthLib } from 'bitdepth';
31
import * as imaadpcm from 'imaadpcm';
32
import * as alawmulaw from 'alawmulaw';
33
import { unpackArray, unpackArrayTo } from 'byte-data';
34
import { WaveFileMetaEditor } from './wavefile-meta-editor';
35
import { truncateSamples, truncateIntSamples } from './truncate-samples';
36
import { resample } from './resampler';
37
38
/**
39
 * A class to convert wav files to other types of wav files.
40
 * @extends WaveFileMetaEditor
41
 * @ignore
42
 */
43
export class WaveFileConverter extends WaveFileMetaEditor {
44
45
  /**
46
   * Force a file as RIFF.
47
   */
48
  toRIFF() {
49
    this.fromExisting_(
50
      this.fmt.numChannels,
51
      this.fmt.sampleRate,
52
      this.bitDepth,
53
      unpackArray(this.data.samples, this.dataType));
54
  }
55
56
  /**
57
   * Force a file as RIFX.
58
   */
59
  toRIFX() {
60
    this.fromExisting_(
61
      this.fmt.numChannels,
62
      this.fmt.sampleRate,
63
      this.bitDepth,
64
      unpackArray(this.data.samples, this.dataType),
65
      {container: 'RIFX'});
66
  }
67
68
  /**
69
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
70
   * @throws {Error} If sample rate is not 8000.
71
   * @throws {Error} If number of channels is not 1.
72
   */
73
  toIMAADPCM() {
74
    if (this.fmt.sampleRate !== 8000) {
75
      throw new Error(
76
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
77
    } else if (this.fmt.numChannels !== 1) {
78
      throw new Error(
79
        'Only mono files can be compressed as IMA-ADPCM.');
80
    } else {
81
      this.assure16Bit_();
82
      /** @type {!Int16Array} */
83
      let output = new Int16Array(this.outputSize_());
84
      unpackArrayTo(this.data.samples, this.dataType, output);
85
      this.fromExisting_(
86
        this.fmt.numChannels,
87
        this.fmt.sampleRate,
88
        '4',
89
        imaadpcm.encode(output),
90
        {container: this.correctContainer_()});
91
    }
92
  }
93
94
  /**
95
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
96
   * @param {string} bitDepthCode The new bit depth of the samples.
97
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
98
   *    Optional. Default is 16.
99
   */
100
  fromIMAADPCM(bitDepthCode='16') {
101
    this.fromExisting_(
102
      this.fmt.numChannels,
103
      this.fmt.sampleRate,
104
      '16',
105
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
106
      {container: this.correctContainer_()});
107
    if (bitDepthCode != '16') {
108
      this.toBitDepth(bitDepthCode);
109
    }
110
  }
111
112
  /**
113
   * Encode a 16-bit wave file as 8-bit A-Law.
114
   */
115
  toALaw() {
116
    this.assure16Bit_();
117
    /** @type {!Int16Array} */
118
    let output = new Int16Array(this.outputSize_());
119
    unpackArrayTo(this.data.samples, this.dataType, output);
120
    this.fromExisting_(
121
      this.fmt.numChannels,
122
      this.fmt.sampleRate,
123
      '8a',
124
      alawmulaw.alaw.encode(output),
125
      {container: this.correctContainer_()});
126
  }
127
128
  /**
129
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
130
   * @param {string} bitDepthCode The new bit depth of the samples.
131
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
132
   *    Optional. Default is 16.
133
   */
134
  fromALaw(bitDepthCode='16') {
135
    this.fromExisting_(
136
      this.fmt.numChannels,
137
      this.fmt.sampleRate,
138
      '16',
139
      alawmulaw.alaw.decode(this.data.samples),
140
      {container: this.correctContainer_()});
141
    if (bitDepthCode != '16') {
142
      this.toBitDepth(bitDepthCode);
143
    }
144
  }
145
146
  /**
147
   * Encode 16-bit wave file as 8-bit mu-Law.
148
   */
149
  toMuLaw() {
150
    this.assure16Bit_();
151
    /** @type {!Int16Array} */
152
    let output = new Int16Array(this.outputSize_());
153
    unpackArrayTo(this.data.samples, this.dataType, output);
154
    this.fromExisting_(
155
      this.fmt.numChannels,
156
      this.fmt.sampleRate,
157
      '8m',
158
      alawmulaw.mulaw.encode(output),
159
      {container: this.correctContainer_()});
160
  }
161
162
  /**
163
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
164
   * @param {string} bitDepthCode The new bit depth of the samples.
165
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
166
   *    Optional. Default is 16.
167
   */
168
  fromMuLaw(bitDepthCode='16') {
169
    this.fromExisting_(
170
      this.fmt.numChannels,
171
      this.fmt.sampleRate,
172
      '16',
173
      alawmulaw.mulaw.decode(this.data.samples),
174
      {container: this.correctContainer_()});
175
    if (bitDepthCode != '16') {
176
      this.toBitDepth(bitDepthCode);
177
    }
178
  }
179
180
  /**
181
   * Change the bit depth of the samples.
182
   * @param {string} newBitDepth The new bit depth of the samples.
183
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
184
   * @param {boolean} changeResolution A boolean indicating if the
185
   *    resolution of samples should be actually changed or not.
186
   * @throws {Error} If the bit depth is not valid.
187
   */
188
  toBitDepth(newBitDepth, changeResolution=true) {
189
    /** @type {string} */
190
    let toBitDepth = newBitDepth;
191
    /** @type {string} */
192
    let thisBitDepth = this.bitDepth;
193
    if (!changeResolution) {
194
      if (newBitDepth != '32f') {
195
        toBitDepth = this.dataType.bits.toString();
196
      }
197
      thisBitDepth = '' + this.dataType.bits;
198
    }
199
    this.assureUncompressed_();
200
    /** @type {number} */
201
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
202
    /** @type {!Float64Array} */
203
    let typedSamplesInput = new Float64Array(sampleCount);
204
    /** @type {!Float64Array} */
205
    let typedSamplesOutput = new Float64Array(sampleCount);
206
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
207
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
208
      truncateSamples(typedSamplesInput);
209
    }
210
    bitDepthLib(
211
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
212
    this.fromExisting_(
213
      this.fmt.numChannels,
214
      this.fmt.sampleRate,
215
      newBitDepth,
216
      typedSamplesOutput,
217
      {container: this.correctContainer_()});
218
  }
219
220
  /**
221
   * Convert the sample rate of the file.
222
   * @param {number} sampleRate The target sample rate.
223
   * @param {?Object} details The extra configuration, if needed.
224
   */
225
  toSampleRate(sampleRate, details={}) {
226
    /** @type {!Array|!TypedArray} */
227
    let samples = this.getSamples();
228
    /** @type {!Array|!Float64Array} */
229
    let newSamples = [];
230
    // Mono files
231
    if (samples.constructor === Float64Array) {
232
      newSamples = resample(samples, this.fmt.sampleRate, sampleRate, details);
233
    // Multi-channel files
234
    } else {
235
      for (let i = 0; i < samples.length; i++) {
236
        newSamples.push(resample(
237
          samples[i], this.fmt.sampleRate, sampleRate, details));
238
      }
239
    }
240
    // Truncate samples
241
    if (this.bitDepth !== '64' && this.bitDepth !== '32f') {
242
      if (newSamples[0].constructor === Number) {
243
        truncateIntSamples(newSamples, this.dataType.bits);
244
      } else {
245
        for (let i = 0; i < newSamples.length; i++) {
246
          truncateIntSamples(newSamples[i], this.dataType.bits);
247
        }
248
      }
249
    }
250
    // Recreate the file
251
    this.fromExisting_(
252
      this.fmt.numChannels, sampleRate, this.bitDepth, newSamples,
253
      {'container': this.correctContainer_()});
254
  }
255
256
  /**
257
   * Make the file 16-bit if it is not.
258
   * @private
259
   */
260
  assure16Bit_() {
261
    this.assureUncompressed_();
262
    if (this.bitDepth != '16') {
263
      this.toBitDepth('16');
264
    }
265
  }
266
267
  /**
268
   * Uncompress the samples in case of a compressed file.
269
   * @private
270
   */
271
  assureUncompressed_() {
272
    if (this.bitDepth == '8a') {
273
      this.fromALaw();
274
    } else if (this.bitDepth == '8m') {
275
      this.fromMuLaw();
276
    } else if (this.bitDepth == '4') {
277
      this.fromIMAADPCM();
278
    }
279
  }
280
281
  /**
282
   * Return 'RIFF' if the container is 'RF64', the current container name
283
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
284
   * @return {string}
285
   * @private
286
   */
287
  correctContainer_() {
288
    return this.container == 'RF64' ? 'RIFF' : this.container;
289
  }
290
291
  /**
292
   * Set up the WaveFileCreator object based on the arguments passed.
293
   * This method only reset the fmt , fact, ds64 and data chunks.
294
   * @param {number} numChannels The number of channels
295
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
296
   * @param {number} sampleRate The sample rate.
297
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
298
   * @param {string} bitDepthCode The audio bit depth code.
299
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
300
   *    or any value between '8' and '32' (like '12').
301
   * @param {!Array|!TypedArray} samples
302
   *    The samples. Must be in the correct range according to the bit depth.
303
   * @param {?Object} options Optional. Used to force the container
304
   *    as RIFX with {'container': 'RIFX'}
305
   * @throws {Error} If any argument does not meet the criteria.
306
   * @private
307
   */
308
  fromExisting_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
309
    let tmpWav = new WaveFileMetaEditor();
310
    Object.assign(this.fmt, tmpWav.fmt);
311
    Object.assign(this.fact, tmpWav.fact);
312
    Object.assign(this.ds64, tmpWav.ds64);
313
    Object.assign(this.data, tmpWav.data);
314
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
315
  }
316
317
  /**
318
   * Return the size in bytes of the output sample array when applying
319
   * compression to 16-bit samples.
320
   * @return {number}
321
   * @private
322
   */
323
  outputSize_() {
324
    /** @type {number} */
325
    let outputSize = this.data.samples.length / 2;
326
    if (outputSize % 2) {
327
      outputSize++;
328
    }
329
    return outputSize;
330
  }
331
}
332